package suncertify.contractors;

import java.util.Map;

import suncertify.application.ApplicationMode;
import suncertify.application.ClientApplication;
import suncertify.application.Log;
import suncertify.application.Strings;
import suncertify.contractors.exceptions.CloseContractorsException;
import suncertify.contractors.exceptions.ContractorFindException;
import suncertify.contractors.exceptions.ContractorUpdateException;
import suncertify.contractors.exceptions.ContractorsInitializationException;
import suncertify.contractors.exceptions.ReadAllContractorsException;
import suncertify.db.DBAccess;
import suncertify.db.Data;
import suncertify.db.DatabaseAccessException;
import suncertify.db.DatabaseCloseException;
import suncertify.db.DatabaseFormatException;
import suncertify.db.DuplicateKeyUncheckedException;
import suncertify.db.FindByCriteriaRuntimeException;
import suncertify.db.RecordAlreadyBookedUncheckedException;
import suncertify.db.RecordNotFoundException;
import suncertify.db.RecordNotFoundUncheckedException;
import suncertify.db.RecordNotLockedUncheckedException;

/**
 * Provides an implementation for the {@link ContractorsAccess} interface. This class should be created when the
 * application runs in {@link ApplicationMode#STANDALONE_CLIENT} and {@link ApplicationMode#SERVER} mode, only one
 * instance of this class should be created in either modes.
 * 
 * <p>
 * The class wraps the {@link Data} class which provides the applications data, the class delegates most its function
 * calls to the corresponding functions in the internal {@link Data} class instance. Most public methods in this class
 * have a corresponding method in the {@link Data} class, exceptions thrown by the <code>Data</code> class methods are
 * transformed into their corresponding exceptions defined in the {@link suncertify.contractors.exceptions} package. The
 * {@link Contractor#getID()} represents the <code>recNo</code> parameter used in the {@link Data} methods.
 * 
 * <p>
 * The class does not have any internal state other than one instance of the {@link Data} class. An instance of this
 * class represents one instance of the {@link Data} class which in turn represents the database records. When this
 * class is instantiated then all records of the database are read and cached in the internal <code>Data</code> class.
 * 
 * @see Data
 * @see DBAccess
 * @see ContractorsAccess
 * @see ClientApplication
 * 
 */
public class ContractorsAccessImpl implements ContractorsAccess {

    /**
     * Reference to the internal <code>Data</code> object.
     */
    private Data data;

    /**
     * Constructs the class using the file path to the applications database. Uses the <code>filePath</code> to
     * instantiate the internal {@link #data} class. If the constructor succeeds then the database file records are all
     * loaded in the internal <code>Data</code> class.
     * 
     * @param filePath
     *            Path to the application's database file.
     * @throws ContractorsInitializationException
     *             Thrown when the {@link Data} class' constructor fails e.g. when the database file access fails or
     *             when the database file is corrupt.
     */
    public ContractorsAccessImpl(String filePath) throws ContractorsInitializationException {

	try {
	    this.data = new Data(filePath);
	    Log.logInfo("ContractorsAccessImpl constructor finished successfully.");
	} catch (DatabaseFormatException e) {

	    Log.logInfo("DatabaseFormatException in ContractorsAccessImpl constructor");
	    throw new ContractorsInitializationException(Strings.MSG_CONTRACTORS_FORMAT_ERROR, e);
	} catch (DatabaseAccessException e) {

	    Log.logInfo("DatabaseAccessException in ContractorsAccessImpl constructor");
	    throw new ContractorsInitializationException(Strings.MSG_CONTRACTORS_ACCESS_ERROR, e);
	}
    }

    /**
     * Returns an array of {@link Contractor} objects based on a search criteria. If nothing matches the criteria then a
     * zero length array is returned. The search criteria is defined by {@link Data#findByCriteria(String[])}.
     * 
     * <p>
     * The function passes the <code>criteria</code> parameter to the {@link Data#findByCriteria(String[])} function and
     * gets a list of record IDs that match the criteria, the data for each record ID is retrieved using the
     * {@link Data#readRecord(long)} function.
     * 
     * @param criteria
     *            The search criteria, see {@link Data#findByCriteria(String[])} for details.
     * @return An array of <code>Contractor</code> objects that match the search criteria.
     * 
     * @throws ContractorFindException
     *             Thrown when the search fails, the exception contains the original cause of the failure.
     * 
     * @throws IllegalStateException
     *             Thrown if the server is stopped while the function is executing.
     */
    public Contractor[] findByCriteria(String[] criteria) throws ContractorFindException {

	// array for the matching record numbers.
	long[] contractorIDs;

	try {
	    // delegate the search to the Data class.
	    contractorIDs = this.data.findByCriteria(criteria);
	} catch (FindByCriteriaRuntimeException e) {
	    Log.logInfo("FindByCriteriaRuntimeException caught in Contractor.findByCriteria()!");
	    throw new ContractorFindException(Strings.MSG_FIND_CONTRACTOR_FAILED, e);
	}

	// allocate an array with the same size as the record number array size.
	Contractor[] contractors = new Contractor[contractorIDs.length];
	String[] contractorArray = null;

	// get the record data for each matching record ID
	for (int index = 0; index < contractorIDs.length; ++index) {

	    try {
		contractorArray = this.data.readRecord(contractorIDs[index]);
	    } catch (RecordNotFoundException e) {

		Log.logInfo("RecordNotFoundException caught in Contractor.findByCriteria()!");

		String message = Strings.getMessage(Strings.MSG_CONTRACTOR_NOTFOUND, Long
			.toString(contractorIDs[index]));

		throw new ContractorFindException(message, e);
	    }

	    // create the contractor object for the given ID and data.
	    contractors[index] = new Contractor(contractorIDs[index], contractorArray);
	}

	return contractors;
    }

    /**
     * Saves the given contractor in the database using the {@link Data#updateRecord(long, String[], long)} method as
     * follows:
     * 
     * <p>
     * <ol>
     * <li>The {@link Data#lockRecord(long)} function is called at the beginning to lock the contractor's record first.
     * The {@link Contractor#getID()} represents the contractor's record.</li>
     * <li>After the contractor's record is locked successfully the {@link Data#updateRecord(long, String[], long)}
     * method is called using the already obtained lock cookie. The {@link Contractor#toStringArray()} is used to get
     * the record string array.</li>
     * <li>After a successful lock the {@link Data#unlock(long, long)} is always called to unlock the record.</li>
     * 
     * <p>
     * All exceptions raised by {@link Data#lockRecord(long)}, {@link Data#unlock(long, long)} and
     * {@link Data#updateRecord(long, String[], long)} are caught and logged, the exceptions are re-raised as defined
     * below.
     * 
     * <p>
     * This function hides many {@link Data} low level exceptions like {@link suncertify.db.SecurityException} and
     * {@link RecordNotLockedUncheckedException} from the caller, these and other exceptions will not happen in the
     * context of this function. If the application supports more features in the future like addition and deletion of
     * contractors then this function should throw more specific exceptions that define the cause of the error e.g.
     * UpdateDeletedContractorException could be caught to inform the user that an attempt was made to update a deleted
     * contractor.
     * 
     * @param newContractor
     *            The contractor to be saved in the database.
     * 
     * @throws ContractorUpdateException
     *             Thrown if any of these exceptions are caught: {@link RecordNotFoundException},{@link Exception},
     *             {@link RecordAlreadyBookedUncheckedException}, {@link suncertify.db.SecurityException},
     *             {@link DuplicateKeyUncheckedException}, {@link RecordNotLockedUncheckedException},
     *             {@link IllegalArgumentException}.
     * 
     * @throws IllegalStateException
     *             Thrown if the server is stopped while the function is executing. Only thrown in network mode.
     */
    public void updateContractor(Contractor newContractor) throws ContractorUpdateException {

	if (newContractor == null) {
	    throw new IllegalArgumentException("newContractor=null!");
	}

	long lockCookie = -1;
	String message = Strings.getMessage(Strings.MSG_CONTRACTOR_UPDATE_ERROR, newContractor.toString());

	// First lock the contractor and obtain the lockCookie.
	try {
	    lockCookie = this.data.lockRecord(newContractor.getID());
	} catch (RecordNotFoundException e) {
	    // should never happen because records cannot be deleted in the database in this version.
	    Log.logInfo("ContractorsAccessImpl.updateContractor() caught exception: " + e.getMessage());
	    throw new ContractorUpdateException(message, e);
	} catch (IllegalStateException e) {
	    // Only happens when the server is stopped while the function was executing.
	    Log.logInfo("ContractorsAccessImpl.updateContractor() caught exception: " + e.getMessage());
	    throw e;
	} catch (Exception e) {
	    // just in case any other unkown exception happens.
	    Log.logInfo("ContractorsAccessImpl.updateContractor() caught exception: " + e.getMessage());
	    throw new ContractorUpdateException(message, e);
	}

	// Contractor is locked now we can update the contractor.
	String[] dataArray = newContractor.toStringArray();

	try {
	    this.data.updateRecord(newContractor.getID(), dataArray, lockCookie);
	} catch (RecordAlreadyBookedUncheckedException e) {
	    // Happens in network mode when a client tries to book a contractor that is already booked.
	    Log.logInfo("ContractorsAccessImpl.updateContractor() caught exception: " + e.getMessage());
	    throw new ContractorUpdateException(message, e);
	} catch (RecordNotFoundException e) {
	    // should not happen in this version because contractors cannot be deleted.
	    Log.logInfo("ContractorsAccessImpl.updateContractor() caught exception: " + e.getMessage());
	    throw new ContractorUpdateException(message, e);
	} catch (suncertify.db.SecurityException e) {
	    // should not happen because this function always uses the correct cookie.
	    Log.logInfo("ContractorsAccessImpl.updateContractor() caught exception: " + e.getMessage());
	    throw new ContractorUpdateException(message, e);
	} catch (DuplicateKeyUncheckedException e) {
	    // will not happen in this version because there is no add or delete implementation, also the name and
	    // location of existing contractors cannot be changed in the update method.
	    Log.logInfo("ContractorsAccessImpl.updateContractor() caught exception: " + e.getMessage());
	    throw new ContractorUpdateException(message, e);
	} catch (RecordNotLockedUncheckedException e) {
	    // Will not happen because the record is always locked in this function.
	    Log.logInfo("ContractorsAccessImpl.updateContractor() caught exception: " + e.getMessage());
	    throw new ContractorUpdateException(message, e);
	} catch (IllegalArgumentException e) {
	    // will not happen because only valid contractors are used which are originally read from the database.
	    Log.logInfo("ContractorsAccessImpl.updateContractor() caught exception: " + e.getMessage());
	    throw new ContractorUpdateException(message, e);
	} catch (IllegalStateException e) {
	    // Server is stopped, terminated or has crashed, callers can react on this exception and show a message.
	    Log.logInfo("ContractorsAccessImpl.updateContractor() caught exception: " + e.getMessage());
	    throw e;
	} finally {

	    try {
		this.data.unlock(newContractor.getID(), lockCookie);
	    } catch (RecordNotFoundUncheckedException e) {
		// should not happen in this version because record cannot be deleted.
		Log.logInfo("ContractorsAccessImpl.updateContractor() caught exception: " + e.getMessage());
		throw new ContractorUpdateException(message, e);
	    } catch (suncertify.db.SecurityException e) {
		// should not happen because update is always called with the correct cookie.
		Log.logInfo("ContractorsAccessImpl.updateContractor() caught exception: " + e.getMessage());
		throw new ContractorUpdateException(message, e);
	    } catch (RecordNotLockedUncheckedException e) {
		// should not happen because update is always called with the correct cookie.
		Log.logInfo("ContractorsAccessImpl.updateContractor() caught exception: " + e.getMessage());
		throw new ContractorUpdateException(message, e);
	    } catch (IllegalStateException e) {
		// only happens when the server is stopped while the unlock function was executing.
		Log.logInfo("ContractorsAccessImpl.updateContractor() caught exception: " + e.getMessage());
		throw e;
	    }
	}
    }

    /**
     * Returns an array of all undeleted {@link Contractor} objects from the database. Each contractor represents one
     * record in the database file. The function delegates the call to {@link Data#getAllRecords()}.
     * 
     * @return An array of {@link Contractor} objects.
     * @throws ReadAllContractorsException
     *             Thrown if the retrieval of all contractors fails.
     * 
     * @throws IllegalStateException
     *             Thrown if the server is stopped while the function is executing. Only thrown in network mode.
     */
    public Contractor[] getAllContractors() throws ReadAllContractorsException {

	try {
	    Map<Long, String[]> records = this.data.getAllRecords();
	    Contractor[] contractors = new Contractor[records.size()];

	    int index = 0;

	    for (Long key : records.keySet()) {
		contractors[index++] = new Contractor(key, records.get(key));
	    }

	    return contractors;
	} catch (Exception ex) {
	    throw new ReadAllContractorsException(ex);
	}
    }

    /**
     * Closes the connection to the applications database file. Delegates the call to {@link Data#close()}.
     * 
     * @throws CloseContractorsException
     *             If any errors occur during the close.
     * 
     */
    public void closeContractors() throws CloseContractorsException {
	try {
	    this.data.close();

	} catch (DatabaseCloseException e) {
	    throw new CloseContractorsException(e);
	}
    }
}
